﻿namespace Fiddler.AMF
{
    using System;
    using System.Collections.Generic;
    using System.Dynamic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Xml;

    class Amf3Deserializer
    {
        internal static readonly DateTime BaseDate = new DateTime(1970, 1, 1);
        private List<string> referenceStrings = new List<string>();
        private List<object> referenceObjects = new List<object>();
        private List<AmfTypeDefinition> referenceDefinitions = new List<AmfTypeDefinition>();
        private Stream stream;
        private BinaryReader reader;

        private List<Dictionary<string, object>> referencedObjects = new List<Dictionary<string, object>>();

        public Amf3Deserializer(Stream input)
            : this(new BinaryReader(input))
        {
        }

        internal Amf3Deserializer(BinaryReader reader)
        {
            this.reader = reader;
            this.stream = reader.BaseStream;
        }

        internal object ReadData(byte? type = null)
        {
            if (type == null)
            {
                type = this.reader.ReadByte();
            }

            Amf3DataType t = (Amf3DataType)type;
            switch (t)
            {
                case Amf3DataType.Undefined:
                    return null;
                case Amf3DataType.Null:
                    return null;
                case Amf3DataType.BooleanFalse:
                    return false;
                case Amf3DataType.BooleanTrue:
                    return true;
                case Amf3DataType.Integer:
                    return this.ReadInteger();
                case Amf3DataType.Number:
                    return this.ReadDouble();
                case Amf3DataType.String:
                    return this.ReadString();
                case Amf3DataType.Xml:
                case Amf3DataType.XmlString:
                    return this.ReadXmlString();
                case Amf3DataType.Date:
                    return this.ReadDate();
                case Amf3DataType.Array:
                    return this.ReadArray();
                case Amf3DataType.Object:
                    return this.ReadObject();
                case Amf3DataType.ByteArray:
                    return this.ReadByteArray();
                case Amf3DataType.VectorInt32:
                    return this.ReadVectorInt32();
                case Amf3DataType.VectorUInt32:
                    return this.ReadVectorUInt32();
                case Amf3DataType.VectorDouble:
                    return this.ReadVectorDouble();
                case Amf3DataType.VectorObject:
                    return this.ReadVectorObject();
                case Amf3DataType.Dictionary:
                    return this.ReadDictionary();
                default:
                    break;
            }

            return null;
        }

        private object ReadDictionary()
        {
            throw new NotImplementedException();
        }

        private object ReadVectorObject()
        {
            throw new NotImplementedException();
        }

        private object ReadVectorDouble()
        {
            throw new NotImplementedException();
        }

        private object ReadVectorUInt32()
        {
            throw new NotImplementedException();
        }

        private object ReadVectorInt32()
        {
            throw new NotImplementedException();
        }

        private object ReadByteArray()
        {
            throw new NotImplementedException();
        }

        private object ReadObject()
        {

            var traitsInfo = this.ReadInteger();
            var storedObject = (traitsInfo & 0x01) == 0;
            traitsInfo = traitsInfo >> 1;

            // Check if the Object is in the stored Objects reference table
            dynamic returnObject;
            if (storedObject)
            {
                var refId = traitsInfo;
                returnObject = this.referenceObjects[refId];
            }
            else
            {
                // Check if the Object is in the stored Definitions reference table
                var storedClass = (traitsInfo & 0x01) == 0;
                string className;
                int encoding;
                List<string> propertyNames;
                traitsInfo = traitsInfo >> 1;
                if (storedClass)
                {
                    var refId = traitsInfo;
                    // Populate the reference attributes
                    var definition = this.referenceDefinitions[refId];
                    className = definition.ClassName;
                    encoding = definition.Encoding;
                    propertyNames = definition.PropertyNames;
                }
                else
                {
                    // The class was not in the reference tables. Start reading rawdata to build traits.
                    // Create a traits table. Zend_Amf_Value_TraitsInfo would be ideal
                    className = this.ReadString();
                    encoding = traitsInfo & 0x03;
                    propertyNames = new List<string>();
                    traitsInfo = traitsInfo >> 2;
                }

                // We now have the object traits defined in variables. Time to go to work:
                if (!string.IsNullOrEmpty(className))
                {
                    // No class name generic object
                    returnObject = new AmfProxy();
                }
                else
                {
                    // Defined object
                    // Typed object lookup against registered classname maps
                    //if (loader = Zend_Amf_Parse_TypeLoader::loadType(className)) {
                    //    returnObject = new loader();
                    //} else {
                    //    //user defined typed object
                    //    require_once 'Zend/Amf/Exception.php';
                    //    throw new Zend_Amf_Exception('Typed object not found: '. className . ' ');
                    //}


                    returnObject = new AmfProxy();
                }

                // Add the Object to the reference table
                this.referenceObjects.Add(returnObject);

                var properties = new Dictionary<string, object>(); // clear value
                // Check encoding types for additional processing.
                switch ((AmfObjectEncoding)encoding)
                {
                    case AmfObjectEncoding.External:
                        // Externalizable object such as {ArrayCollection} and {ObjectProxy}
                        if (!storedClass)
                        {
                            this.referenceDefinitions.Add(new AmfTypeDefinition
                            {
                                ClassName = className,
                                Encoding = encoding,
                                PropertyNames = propertyNames
                            });
                        }

                        returnObject.ExternalizedData = this.ReadData();
                        break;
                    case AmfObjectEncoding.Dynamic:
                        // used for Name-value encoding
                        if (!storedClass)
                        {
                            this.referenceDefinitions.Add(new AmfTypeDefinition
                            {
                                ClassName = className,
                                Encoding = encoding,
                                PropertyNames = propertyNames
                            });
                        }

                        // not a reference object read name value properties from byte stream
                        string property;
                        do
                        {
                            property = this.ReadString();
                            if (!string.IsNullOrEmpty(property))
                            {
                                propertyNames.Add(property);
                                properties[property] = this.ReadData();
                            }
                        }
                        while (!string.IsNullOrEmpty(property));
                        break;
                    default:
                        // basic property list object.
                        if (!storedClass)
                        {
                            var count = traitsInfo; // Number of properties in the list
                            for (var i = 0; i < count; i++)
                            {
                                propertyNames.Add(this.ReadString());
                            }
                            // Add a reference to the class.
                            this.referenceDefinitions.Add(new AmfTypeDefinition
                            {
                                ClassName = className,
                                Encoding = encoding,
                                PropertyNames = propertyNames
                            });
                        }
                        foreach (var propertyName in propertyNames)
                        {
                            properties[propertyName] = this.ReadData();
                        }
                        break;
                }

                // Add properties back to the return object.
                returnObject.SetProperties(properties);
            }

            //if (returnObject instanceof Zend_Amf_Value_Messaging_ArrayCollection) {
            //     if (isset(returnObject->externalizedData)) {
            //         returnObject = returnObject->externalizedData;
            //     } else {
            //         returnObject = get_object_vars(returnObject);
            //     }
            //}

            return returnObject;
        }

        private AmfProxy ReadArray()
        {
            var arrayReference = this.ReadInteger();
            if ((arrayReference & 0x01) == 0)
            {
                arrayReference = arrayReference >> 1;
                return (AmfProxy)this.referenceObjects[arrayReference];
            }

            // Create a holder for the array in the reference list
            var data = new List<object>();
            var props = new Dictionary<String, object>();

            var key = this.ReadString();

            // Iterating for string based keys.
            while (key != string.Empty)
            {
                props[key] = this.ReadData();
                key = this.ReadString();
            }

            arrayReference = arrayReference >> 1;

            //We have a dense array
            for (var i = 0; i < arrayReference; i++)
            {
                data.Add(this.ReadData());
            }

            var result = new AmfProxy();
            result.SetProperties(props);
            result.SetValues(data);
            this.referenceObjects.Add(result);
            return result;
        }

        private object ReadDate()
        {
            throw new NotImplementedException();
        }

        private object ReadXmlString()
        {
            throw new NotImplementedException();
        }

        private string ReadString()
        {
            var stringReference = this.ReadInteger();

            //Check if this is a reference string
            if ((stringReference & 0x01) == 0)
            {
                // reference string
                stringReference = stringReference >> 1;
                if (stringReference >= this.referenceStrings.Count)
                {
                    throw new FormatException("Undefined string reference: " + stringReference);
                }
                // reference string found
                return this.referenceStrings[stringReference];
            }

            var length = stringReference >> 1;
            if (length != 0)
            {
                var stringBytes = this.reader.ReadBytes(length);
                var result = Encoding.UTF8.GetString(stringBytes);
                this.referenceStrings.Add(result);
                return result;
            }
            return string.Empty;
        }

        private double ReadDouble()
        {
            return this.reader.ReadDouble();
        }

        private int ReadInteger()
        {
            var count = 1;
            var intReference = this.reader.ReadByte();
            var result = 0;
            while (((intReference & 0x80) != 0) && count < 4)
            {
                result <<= 7;
                result |= (intReference & 0x7f);
                intReference = this.reader.ReadByte();
                count++;
            }
            if (count < 4)
            {
                result <<= 7;
                result |= intReference;
            }
            else
            {
                // Use all 8 bits from the 4th byte
                result <<= 8;
                result |= intReference;

                // Check if the integer should be negative
                if ((result & 0x10000000) != 0)
                {
                    //and extend the sign bit
                    result |= ~0xFFFFFFF;
                }
            }
            return result;
        }

        class AmfTypeDefinition
        {
            public string ClassName { get; set; }
            public int Encoding { get; set; }
            public List<string> PropertyNames { get; set; }
        }

        class AmfProxy : DynamicObject
        {    // The inner dictionary.
            Dictionary<string, object> dictionary = new Dictionary<string, object>();
            List<object> values = new List<object>();

            public AmfProxy()
            {
            }

            public AmfProxy(Dictionary<string, object> propertyValues)
            {
                SetProperties(propertyValues);
            }

            public void SetProperties(Dictionary<string, object> propertyValues)
            {
                this.dictionary = propertyValues;
            }

            public void SetValues(List<object> propertyValues)
            {
                this.values = propertyValues;
            }

            // This property returns the number of elements
            // in the inner dictionary.
            public int Count
            {
                get
                {
                    return dictionary.Count;
                }
            }

            public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
            {
                if (indexes.Length != 1)
                {
                    result = null;
                    return false;
                }

                if (indexes[0] is int)
                {
                    var intIndex = (int)indexes[0];
                    result = this.values[intIndex];
                    return true;
                }

                if (indexes[0] is string)
                {
                    var name = (string)indexes[0];
                    return dictionary.TryGetValue(name, out result);
                }

                throw new NotImplementedException();
            }

            // If you try to get a value of a property 
            // not defined in the class, this method is called.
            public override bool TryGetMember(GetMemberBinder binder, out object result)
            {
                // Converting the property name to lowercase
                // so that property names become case-insensitive.
                string name = binder.Name.ToLower();

                // If the property name is found in a dictionary,
                // set the result parameter to the property value and return true.
                // Otherwise, return false.
                return dictionary.TryGetValue(name, out result);
            }

            // If you try to set a value of a property that is
            // not defined in the class, this method is called.
            public override bool TrySetMember(
                SetMemberBinder binder, object value)
            {
                // Converting the property name to lowercase
                // so that property names become case-insensitive.
                dictionary[binder.Name.ToLower()] = value;

                // You can always add a value to a dictionary,
                // so this method always returns true.
                return true;
            }

        }
    }
}